<?php


namespace app\mini\controller\v1;


use app\common\cachemodel\MiniUser;
use app\common\lib\exception\ApiException;
use app\common\lib\IAuth;
use app\common\lib\mini\alipay\AmpHelper;
use app\common\lib\Util;
use Exception;
use GuzzleHttp\Exception\GuzzleException;
use think\Cache;
use think\Controller;
use think\Log;
use think\response\Json;

class Login extends Controller
{

    private const LOG_PRE = '小程序Login Controller 异常：';

    /**
     * 百度小程序保存到redis的access_token的key
     */
    private const SWAN_ACCESS_TOKEN_KEY = 'mini:swan_access_token';

    /**
     * 登录接口
     * @throws ApiException
     * @throws Exception
     * @throws GuzzleException
     * @throws Exception
     */
    public function login()
    {
        $env_type = input('env_type/s');
        $code = input('code/s');

        $mini_item_conf = config('mini')[$env_type];
        //由于支付宝小程序的登录比较特殊，需要单做
        if ($env_type === 'ALIPAY')
        {
            $params = [
                'mini_type' => $env_type,
                'code' => $code,
                'avatar' => input('avatar/s'),
                'nickname' => input('nickname/s'),
                'sex' => input('sex/d'),
                'city' => input('city/s'),
                'province' => input('province/s'),
                'country' => input('country_code/s'),
            ];
            $validate = validate('Calendar');
            if (!$validate->scene('alipay')->check($params))
            {
                return show(config('code.error'), (string)$validate->getError());
            }
            //获取user_id，返回结果参考：https://docs.open.alipay.com/api_9/alipay.system.oauth.token
            $tokenData = AmpHelper::getAmpToken($code);
            if (!isset($tokenData['user_id']))
            {
                //记录支付宝通过code获取用户user_id失败的错误日志
                Log::write(self::LOG_PRE . json_encode($tokenData), 'error', true);
                return show(config('code.error'), '获取用户支付宝id失败!');
            }
            return $this->userLogin(array_merge($params, $tokenData));
        }

        $encrypted_data = input('encrypted_data/s');
        $iv = input('iv/s');
        //TT：字节跳动小程序；
        $tt_raw_data = input('tt_raw_data/s');
        $tt_signature = input('tt_signature/s');
        //TT：字节跳动小程序；

        if ('TT' === $env_type)
        {//字节跳动小程序
            $params = [
                'mini_type' => $env_type,
                'code' => $code,
                'iv' => $iv,
                'encrypted_data' => $encrypted_data,
                'tt_raw_data' => $tt_raw_data,
                'tt_signature' => $tt_signature,
            ];
            $validate = validate('Calendar');
            if (!$validate->scene('tt')->check($params))
            {
                return show(config('code.error'), (string)$validate->getError());
            }
        } else
        {//下面是除了支付宝/字节跳动小程序其他小程序
            $params = [
                'mini_type' => $env_type,
                'code' => $code,
                'iv' => $iv,
                'encrypted_data' => $encrypted_data
            ];
            $validate = validate('Calendar');
            if (!$validate->scene('others')->check($params))
            {
                return show(config('code.error'), (string)$validate->getError());
            }
        }

        $session_key = $this->getSessionKey($code, $env_type, $mini_item_conf);
        if (empty($session_key))
        {
            return show(config('code.error'), '获取session_key异常！');
        }
        switch ($env_type)
        {
            case 'TT':
                if ($session_key)
                {
                    $data = $this->ttDecrypt($encrypted_data, $iv, $session_key, $tt_signature, $tt_raw_data);
                    if ($data)
                    {
                        return $this->userLogin(array_merge($params, $data));
                    }
                }
                break;
            case 'SWAN':
                //百度小程序，解密数据，获取用户信息（注意：不包含unionid）
                $data = $this->swanDecrypt($encrypted_data, $iv, $mini_item_conf['app_key'], $session_key);
                if ($data)
                {
                    //获取unionid
                    //先查询redis缓存中是否存在access_token，如果存在那么使用，如果不存在，那么获取并放到redis缓存中
                    $access_token = Cache::get(self::SWAN_ACCESS_TOKEN_KEY);
                    if (empty($access_token) || $access_token === '')
                    {
                        //获取access_token（readme：https://smartprogram.baidu.com/docs/develop/serverapi/power_exp/）
                        $access_token_url = 'https://openapi.baidu.com/oauth/2.0/token?grant_type=client_credentials&client_id=' . $mini_item_conf['app_key'] . '&client_secret=' . $mini_item_conf['app_secret'] . '&scope=smartapp_snsapi_base';
                        $result_of_get_access_token = json_decode(file_get_contents($access_token_url), false);
                        if (isset($result_of_get_access_token->errcode))
                        {
                            Cache::rm(self::SWAN_ACCESS_TOKEN_KEY);
                        } else
                        {
                            $access_token = $result_of_get_access_token->access_token;
                            Cache::set(self::SWAN_ACCESS_TOKEN_KEY, $access_token, $result_of_get_access_token->expires_in);
                        }
                    }

                    //获取unionid（readme：https://smartprogram.baidu.com/docs/develop/api/open/log_getunionid/）
                    $union_url = 'https://openapi.baidu.com/rest/2.0/smartapp/getunionid?access_token=' . $access_token;
                    $body = ['openid' => $data['openid']];
                    $header = ['Content-Type' => 'application/x-www-form-urlencoded'];
                    $result_of_get_unionid = curl_post($union_url, $body, $header);
                    $result_of_get_unionid = json_decode($result_of_get_unionid, true);
                    if (isset($result_of_get_unionid['data'], $result_of_get_unionid['data']['unionid']))
                    {
                        $data_decrypt['unionid'] = $result_of_get_unionid['data']['unionid'];
                        return $this->userLogin(array_merge($data_decrypt, $data, $params));
                    }

                    Cache::rm(self::SWAN_ACCESS_TOKEN_KEY);//如果错误，那么删除access_token，因为有可能是access_token错了
                } else
                {
                    return show(config('code.error'), 'failed');
                }
                break;
            case 'WEAPP':
            case 'QQ':
                $data = $this->wechatOrQQDecrypt($mini_item_conf['app_id'], $session_key, $encrypted_data, $iv);
                if ($data)
                {
                    return $this->userLogin(array_merge($params, $data));
                }
                break;
        }

        return show(config('code.error'), '登录失败！');
    }

    /**
     * 用户登录统一处理
     * @param array $data
     * @return Json|null
     * @throws ApiException
     * @throws GuzzleException
     */
    private function userLogin($data = []): ?Json
    {
        $third_id = '';

        $mini_type = $data['mini_type'];
        $nickname = '';
        $avatar = '';
        $sex = '';
        $unionid = '';
        $openid = '';
        $city = '';
        $province = '';
        $country = '';

        switch ($mini_type)
        {
            case 'ALIPAY':
                $third_id = $data['user_id'];

                $nickname = $data['nickname'];
                $avatar = $data['avatar'];
                $sex = $data['sex'];
                $unionid = '';
                $openid = $third_id;
                $city = $data['city'];
                $province = $data['province'];
                $country = $data['country'];
                break;
            case 'QQ':
            case 'WEAPP':
                //todo 校验app、h5 微信的的unionid是否一样，准了科技的qq没有关联app应用，所以不能获取用户的unionid，所以这里有必要切换为星座女神的那个qq，否则未来没法做关联
                $third_id = $data['unionId'];

                $nickname = $data['nickName'];
                $avatar = $data['avatarUrl'];
                $sex = $data['gender'];
                $unionid = $data['unionId'];
                $openid = $data['openId'];
                $city = $data['city'];
                $province = $data['province'];
                $country = $data['country'];
                break;
            case 'TT':
                $third_id = $data['openId'];

                $nickname = $data['nickName'];
                $avatar = $data['avatarUrl'];
                $sex = $data['gender'];
                $unionid = '';
                $openid = $third_id;
                $city = $data['city'];
                $province = $data['province'];
                $country = $data['country'];
                break;
            case 'SWAN':
                $third_id = $data['unionid'];

                $nickname = $data['nickname'];
                $avatar = $data['headimgurl'];
                $sex = $data['sex'];
                $unionid = $third_id;
                $openid = $data['openid'];
                $city = '';
                $province = '';
                $country = '';
                break;
        }

        try
        {

            $MiniUser = new MiniUser();
            //验证是否注册过
            $mini_user = $MiniUser->getMiniInfo($third_id, $mini_type);
            if ($mini_user)
            {//已经注册过了，返回用户信息
                return show(config('code.success'), '登录成功', ['third_id' => $third_id, 'token' => $mini_user['access_token']]);
            }

            //没有注册过，插入用户信息
            //生成access_token和过期时间
            $token_info = $this->getUserTokenInfo($third_id);

            if ($avatar)
            {
                $avatarInfo = Util::wxImgUploadOss($avatar, 'uploads/avatar');
            }

            if (false === $avatarInfo['success'])
            {
                Log::write(self::LOG_PRE . '  上传三方头像失败，' . $avatarInfo['message'], 'error');
            }

            $avatar = $avatarInfo['success'] ? $avatarInfo['url'] : $avatar;

            $mini_user = [
                'mini_type' => $mini_type,
                'nickname' => $nickname ?? '',
                'avatar' => $avatar,
                'sex' => $sex,
                'unionid' => $unionid,
                'openid' => $openid,
                'city' => $city,
                'province' => $province,
                'country' => $country,
                'access_token' => $token_info['access_token'],
                'expires_in' => $token_info['expires_in'],
                'create_time' => time(),
            ];
            (new MiniUser())->SavaData($mini_user);

            return show(config('code.success'), '登录成功', ['third_id' => $third_id, 'token' => $token_info['access_token']]);
        }
        catch (Exception $e)
        {
            Log::write(self::LOG_PRE . '用户注册/登录失败，' . $e->getMessage() . '失败行数：' . $e->getLine(), 'error');
            throw new ApiException('登录失败', 200, 0);
        }
    }

    /**
     * 获取用户token信息
     * @param string $third_id
     * @return array
     */
    public function getUserTokenInfo($third_id = ''): array
    {
        // 生成token  和 token到期时间
        $token = IAuth::setAppLoginToken($third_id);
        $data = [
            'access_token' => $token,
            'expires_in' => strtotime('+' . config('app.login_time_out_day') . ' days'),
        ];
        return $data;
    }


    /**
     * 微信或者QQ，检验数据的真实性，并且获取解密后的.
     * @param $appId
     * @param $sessionKey
     * @param $encryptedData string 加密的用户数据
     * @param $iv string 与用户数据一同返回的初始向量
     * @return array | int 成功返回详细信息，失败返回false，例如：avatarUrl = "https://wx.qlogo.cn/mmopen/vi_32/DYAIOgq83epF3yvjxqJuRB9KydwTVHqEpia3wxwnBOkFUsDhMV9GwO9vcJ6tHlRbsEuicC9p7e859QCMVcRAt1uA/132"
     * city = "Chaoyang"
     * country = "China"
     * gender = {int} 1
     * language = "zh_CN"
     * nickName = "李柏楠"
     * openId = "oVpmX5KHiQXXabinAnBbYy3Um2ns"
     * province = "Beijing"
     * unionId = "oIVMdt71AOrtGBFSlYBe8zSDl5_o"
     * watermark = {array} [2]
     * appid = "wxa417c0a7a1e75643"
     * timestamp = {int} 1575437969
     */
    private function wechatOrQQDecrypt($appId, $sessionKey, $encryptedData, $iv)
    {
        //qq登录没有unionid，有可能是我的测试号的原因，切换后再测试【解决：使用星座女神的qq号，登录qq开发平台进行的关联】
        if (strlen($sessionKey) !== 24)
        {
            return false;
        }
        $aesKey = base64_decode($sessionKey);
        if (strlen($iv) !== 24)
        {
            return false;
        }
        $aesIV = base64_decode($iv);
        $aesCipher = base64_decode($encryptedData);
        $result = openssl_decrypt($aesCipher, 'AES-128-CBC', $aesKey, 1, $aesIV);
        $data_arr = json_decode($result, true);
        if ($data_arr === NULL)
        {
            return false;
        }
        if ($data_arr['watermark']['appid'] !== $appId)
        {
            return false;
        }
        return $data_arr;
    }

    /**
     * 百度小程序 数据解密：低版本使用mcrypt库（PHP < 5.3.0），高版本使用openssl库（PHP >= 5.3.0）。
     *
     * @param string $ciphertext 待解密数据，返回的内容中的data字段
     * @param string $iv 加密向量，返回的内容中的iv字段
     * @param string $app_key 创建小程序时生成的app_key
     * @param string $session_key 登录的code换得的
     * @return string | false
     * headimgurl = "https://himg.bdimg.com/sys/portrait/item/be756d61726b6c69626e8042"
     * nickname = "百度网友ed3e9fc"
     * openid = "qsN6gzg9LLCWzbmoQIDh-7NovZ"
     * sex = {int} 0
     */
    private function swanDecrypt($ciphertext, $iv, $app_key, $session_key)
    {
        $session_key = base64_decode($session_key);
        $iv = base64_decode($iv);
        $ciphertext = base64_decode($ciphertext);

        if (function_exists('openssl_decrypt'))
        {
            $plaintext = openssl_decrypt($ciphertext, 'AES-192-CBC', $session_key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv);
        } else
        {
            $td = mcrypt_module_open(MCRYPT_RIJNDAEL_128, null, MCRYPT_MODE_CBC, null);
            mcrypt_generic_init($td, $session_key, $iv);
            $plaintext = mdecrypt_generic($td, $ciphertext);
            mcrypt_generic_deinit($td);
            mcrypt_module_close($td);
        }
        if ($plaintext === false)
        {
            return false;
        }

        // trim pkcs#7 padding
        $pad = ord(substr($plaintext, -1));
        $pad = ($pad < 1 || $pad > 32) ? 0 : $pad;
        $plaintext = substr($plaintext, 0, strlen($plaintext) - $pad);

        // trim header
        $plaintext = substr($plaintext, 16);
        // get content length
        $unpack = unpack('Nlen/', substr($plaintext, 0, 4));
        // get content
        $content = substr($plaintext, 4, $unpack['len']);
        // get app_key
        $app_key_decode = substr($plaintext, $unpack['len'] + 4);

        return $app_key === $app_key_decode ? json_decode($content, true) : false;
    }


    /**
     * 今日头条小程序 验数据是否合法，并且解密数据 （解密参考： https://microapp.bytedance.com/dev/cn/mini-app/develop/open-capacity/user-information/sensitive-data-process）
     * @param $ciphertext
     * @param $iv
     * @param $session_key
     * @param $signature
     * @param $rawData
     * @return bool|mixed
     * avatarUrl = "http://sf1-ttcdn-tos.pstatp.com/img/mosaic-legacy/3791/5070639578~120x256.image"
     * city = ""
     * country = "中国"
     * gender = {int} 0
     * language = ""
     * nickName = "zhunle"
     * openId = "2kfTH0Viw4tUSz6y"
     * province = ""
     * watermark = {array} [2]
     * appid = "tta48ad2373ba085a1"
     * timestamp = {int} 1575450447
     */
    private
    function ttDecrypt($ciphertext, $iv, $session_key, $signature, $rawData)
    {
        //先校验数据是否合法
        if ($signature !== sha1($rawData . $session_key))
        {
            return false;
        }

        $session_key = base64_decode($session_key);
        $iv = base64_decode($iv);
        $ciphertext = base64_decode($ciphertext);

        if (function_exists('openssl_decrypt'))
        {
            $plaintext = openssl_decrypt($ciphertext, 'AES-128-CBC', $session_key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv);
        } else
        {
            $td = mcrypt_module_open(MCRYPT_RIJNDAEL_128, null, MCRYPT_MODE_CBC, null);
            mcrypt_generic_init($td, $session_key, $iv);
            $plaintext = mdecrypt_generic($td, $ciphertext);
            mcrypt_generic_deinit($td);
            mcrypt_module_close($td);
        }
        if ($plaintext === false)
        {
            return false;
        }

        // trim pkcs#7 padding
        $pad = ord(substr($plaintext, -1));
        $pad = ($pad < 1 || $pad > 32) ? 0 : $pad;
        $plaintext = substr($plaintext, 0, strlen($plaintext) - $pad);

        return json_decode(trim($plaintext), true);
    }


    /**
     * 获取session key
     * @param $code
     * @param string $env_type SWAN：百度小程序；WEAPP：微信小程序；QQ:qq小程序；TT：字节跳动小程序；
     * @param array $mini_item_conf 单个小程序的配置
     * @return string
     */
    private function getSessionKey($code, $env_type = '', $mini_item_conf = []): string
    {
        try
        {
            switch ($env_type)
            {
                case 'SWAN':
                    $access_token_url = 'https://spapi.baidu.com/oauth/jscode2sessionkey?client_id=' . $mini_item_conf['app_key'] . '&sk=' . $mini_item_conf['app_secret'] . '&code=' . $code;
                    break;
                case 'WEAPP':
                    $access_token_url = 'https://api.weixin.qq.com/sns/jscode2session?appid=' . $mini_item_conf['app_id'] . '&secret=' . $mini_item_conf['app_secret'] . '&js_code=' . $code . '&grant_type=authorization_code';
                    break;
                case 'QQ':
                    $access_token_url = 'https://api.q.qq.com/sns/jscode2session?appid=' . $mini_item_conf['app_id'] . '&secret=' . $mini_item_conf['app_secret'] . '&js_code=' . $code . '&grant_type=authorization_code';
                    break;
                case 'TT':
                    $access_token_url = 'https://developer.toutiao.com/api/apps/jscode2session?appid=' . $mini_item_conf['app_id'] . '&secret=' . $mini_item_conf['app_secret'] . '&code=' . $code;
                    break;
            }
            if (!empty($access_token_url))
            {
                $result = file_get_contents($access_token_url);
                $result_arr = json_decode($result, true);
                if (isset($result_arr['session_key']))
                {
                    return $result_arr['session_key'];
                }
            }
        }
        catch (Exception $e)
        {
            Log::write(self::LOG_PRE . '获取 ' . $env_type . ' session key异常，信息为：' . $e->getMessage() . PHP_EOL, 'error', true);
        }

        return '';
    }
}
